﻿using System;
using System.Collections.Concurrent;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using O1.Kernel;

namespace O1.Storage
{
    [ContractVerification(true)]
    internal class TransactionQueue : IDisposable
    {
        private readonly BlockingCollection<StorageCommand> commandQueue = new BlockingCollection<StorageCommand>();

        private readonly CancellationTokenSource workerCancellation;

        private readonly DifferencingStore storage;

        private readonly SequenceCache32 counter;

        private readonly ConcurrentQueue<int> deleted;

        public TransactionQueue(DifferencingStore storage, int maxValue, int[] deleted)
        {
            Contract.Requires(storage != null);
            Contract.Requires(deleted != null);
            Contract.Requires(maxValue > -1);

            this.storage = storage;

            // Sychronized blocks of 100 are fine. In the event of a failure, the restoration process
            // will 'know' the highest id and will re-initialize the counter to a nearly contiguous value.
            this.counter = new SequenceCache32(new DurableCounter(maxValue), 100);
            this.deleted = new ConcurrentQueue<int>(deleted);
            this.workerCancellation = this.InitializeWorker();
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void EnqueueAdd(TransactionToken token, byte[] change)
        {
            int targetId;
            if (!this.deleted.TryDequeue(out targetId))
            {
                targetId = this.counter.NextId();
            }

            this.commandQueue.Add(new StorageCommand(token, targetId, change));
        }

        public void EnqueueModify(TransactionToken token, int targetId, byte[] change)
        {
            this.commandQueue.Add(new StorageCommand(token, targetId, change));
        }

        protected void Dispose(bool disposing)
        {
            // TODO: Disposal of a backing store is meaningful...
            // What is the persistency implication?
            if (disposing)
            {
                this.counter.Dispose();
                this.workerCancellation.Cancel();
                this.commandQueue.Dispose();
            }
        }

        private CancellationTokenSource InitializeWorker()
        {
            CancellationTokenSource cancellation = new CancellationTokenSource();
            Action pendDml = () =>
                {
                    foreach (var command in this.commandQueue.GetConsumingEnumerable())
                    {
                        // TODO: There is no concurrency check at all.\
                        // Also, the design of having a Storage Task as a class
                        // allows for communication of results back to the calling 
                        // thread. This is needed.
                        this.storage.Save(command.TargetId, command.Delta);
                    }
                };

            Task.Factory.StartNew(pendDml, cancellation.Token);
            return cancellation;
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(this.storage != null);
            Contract.Invariant(this.counter != null);
            Contract.Invariant(this.deleted != null);
            Contract.Invariant(this.commandQueue != null);
            Contract.Invariant(this.workerCancellation != null);
        }

        private struct StorageCommand
        {
            public readonly TransactionToken Token;

            public readonly byte[] Delta;

            public readonly int TargetId;

            public StorageCommand(TransactionToken token, int targetId, byte[] delta)
                : this()
            {
                this.Delta = delta;
                this.Token = token;
                this.TargetId = targetId;
            }
        }
    }
}